<!DOCTYPE html>
<head>
  <meta charset="utf-8" />
  <title>Stockfish NNUE JS</title>
  <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, user-scalable=no" />
</head>

<!-- CSS -->

<style>

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0; padding: 0; border: 0;
  font: inherit;
}

html, body, #root {
  height: 100%;
}

#root {
  font-family: 'mono';
  font-size: 13px;
}

main {
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: 10px;
}

#input {
  display: flex;
  margin-bottom: 10px;
}

#input #command {
  width: 80%;
  padding: 2px 0 2px 6px;
  border: 1px solid #ddd;
  border-right: 0;
}

#input #send {
  display: inline-block;
  padding: 3px 6px 3px 6px;
  background: #888;
  color: #fff;
  font-weight: bold;
  margin-right: 6px;
  cursor: pointer;
}

#input #examples {
  width: 120px;
  background: #eee;
}

#misc {
  margin-bottom: 10px;
}

#output {
  flex: 1 1 auto;
  width: 100%;
  border: 1px solid #ddd;
  padding: 10px;
  overflow: scroll;
  box-shadow: inset 1px 1px 2px #eee;
  font-size: 11px;
}

</style>

<body>
<div id="root"></div>

<!-- Javascript -->

<script src="./stockfish.js"></script>
<script src="./mithril.2.0.4.min.js"></script>
<script>

const $ = (...args) => document.querySelector(...args);

const formatMB = (n) => {
  return (n ? (n / 1e6).toPrecision(3) : '?') + 'MB';
};

const isSupported = () => {
  if (typeof WebAssembly !== 'object') return false;
  const source = Uint8Array.from([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 7, 8, 1, 4, 116, 101, 115, 116, 0, 0, 10, 15, 1, 13, 0, 65, 0, 253, 17, 65, 0, 253, 17, 253, 186, 1, 11]);
  if (typeof WebAssembly.validate !== 'function' || !WebAssembly.validate(source)) return false;
  if (typeof Atomics !== 'object') return false;
  if (typeof SharedArrayBuffer !== 'function') return false;
  return true;
};

const RequestProgress = ({ attrs: { url, onFinishDownload } }) => {
  let state = 'INIT'; // 'LOADING', 'DONE', 'FAILED'
  let loaded = 0;
  let total = 0;

  const oninit = () => {
    state = 'LOADING';
    m.request({
      url: url,
      method: 'GET',
      responseType: 'arraybuffer',
      headers: { Accept: '*/*' },
      config: (xhr) => {
        xhr.onprogress = (e) => {
          // TODO:
          // When gzip compressed, the value of "loaded/total" gets messed up.
          // On Chrome, "loaded" is the value after decompression, but on the other hand,
          // On Firefox, "loaded" is the value before decompression.
          loaded = e.loaded;
          total = e.total || Number(e.target.getResponseHeader("x-decompressed-content-length"));
          m.redraw();
        };
      }
    }).then(
      (response) => { state = 'DONE'; onFinishDownload(response); },
      (e) => { console.error(e); state = 'FAILED'; onFinishDownload(null); }
    )
  };

  const view = () => {
    const fraction = (total == -1) ? `?MB/?MB` : `${formatMB(loaded)}/${formatMB(total)}`;
    return m("span", [
      `${fraction} [${state}] `,
      m("span", {
        style: 'cursor: pointer;',
        onclick: () => window.alert('On some browsers, download size might look contradicted due to file compression.'),
      }, '[?]')
    ]);
  };

  return { oninit, view };
};

const App = () => {
  let stockfish = null;
  let stockfish_state = 'INIT'; // 'READY', 'FAILED'
  let output = '';
  let tail_mode = true;

  const examples = [
    'uci',
    'go',
    'go depth 10',
    'go depth 20',
    'go depth 25',
    'stop',
    'd',
    'eval',
    'position startpos',
    'position fen r1k4r/ppp1bq1p/2n1N3/6B1/3p2Q1/8/PPP2PPP/R5K1 w - - 0 1',
    'setoption name Use NNUE value true',
    'setoption name Use NNUE value false',
    'setoption name Threads value 1',
    'setoption name Threads value 4',
    'bench 16 1 20 current depth NNUE',
    'bench 16 1 20 current depth classical',
    'bench_eval',
    'setoption name EvalFile value old-nn/nn-76a8a7ffb820.nnue',
    'setoption name EvalFile value http://127.0.0.1:8080/old-nn/nn-76a8a7ffb820.nnue',
    'position fen 8/7p/6p1/5p2/Q4P2/2p3P1/3r3P/2K1k3 w - - 2 44 moves a4a7',
  ];

  const sendCommand = () => {
    const command = $('#command').value;
    if (command.length > 0) { stockfish.postMessage(command); }
  };

  const scrollOutput = () => {
    if (!tail_mode) { return; }
    $('#output').scrollTo({ top: $('#output').scrollHeight, behavior: 'smooth' });
  };

  const loadStockfish = function ()
  {
    var mod = {
        locateFile: function (path)
        {
            if (path.indexOf(".wasm") > -1) {
                return "stockfish.wasm";
            } else {
                return "stockfish.js#stockfish.wasm,worker";
                //console.error("TODO: CONCAT worker");
                //return "stockfish.worker.js";
                
                //const code = "(" + '"use strict";var Module={};if(typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string"){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",function(data){onmessage({data:data})});var nodeFS=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(nodeFS.readFileSync(f,"utf8"))},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob==="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}Stockfish(Module).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};' + ")();";
                //const blob = new Blob([code], {type: 'application/javascript'})
                //const code = '"use strict";var Module={};if(typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string"){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",function(data){onmessage({data:data})});var nodeFS=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(nodeFS.readFileSync(f,"utf8"))},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob==="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}Stockfish(Module).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};';
                //const worker = new Worker(URL.createObjectURL(blob))
                //return 'data:application/javascript,' + encodeURIComponent(code);
            }
        }
    };
    Stockfish(mod).then(_stockfish => {
      stockfish = _stockfish;
      stockfish_state = 'READY';
      stockfish.addMessageListener((line) => {
        output += line + '\n';
        m.redraw();
      });
    })
  };
/*
  const onFinishDownload = (data) => {
    if (!data) { stockfish_state = 'FAILED'; m.redraw(); return; }

    loadStockfish({ wasmBinary: data })
    .then(_stockfish => {
      stockfish = _stockfish;
      stockfish_state = 'READY';
      stockfish.addMessageListener((line) => {
        output += line + '\n';
        m.redraw();
      });
    })
    .catch(e => { stockfish_state = 'FAILED'; throw e; })
    .finally(() => m.redraw());
  };
*/
  const oninit = () => {
    stockfish_state = 'READY';
  };
  
  const view = () => {
    const is_ready = stockfish_state == 'READY';
    //
    return m("main", [
      m("div#input", [
        m("input#command", {
          placeholder: 'Input UCI Command Here',
          disabled: !is_ready,
          onkeyup: (e) => {
            if (e.keyCode != 13) { e.redraw = false; return; }
            sendCommand();
          }
        }),
        m("span#send", { disabled: !is_ready, onclick: sendCommand }, 'SEND'),
        m("select#examples", {
          disabled: !is_ready,
          onchange: (e) => { $("#command").value = e.target.value; window.setTimeout(sendCommand, 10); }
        }, [
          m("option", { value: '' }, "-- EXAMPLE --"),
          ...examples.map((ex, index) => m("option", { value: ex }, ex)),
        ])
      ]),
      m("div#misc", [
        //m("div", ['- download: ', m(RequestProgress, { url: './stockfish.wasm', onFinishDownload })]),
        m("div", `- stockfish_state: ${stockfish_state}`),
        m("div", [
          m("span", { style: 'margin-right: 5px;' }, '- tail_mode:'),
          m("span", { style: 'cursor: pointer;', onclick: () => { tail_mode = !tail_mode; scrollOutput(); } }, [
            tail_mode ? '[x]' : '[ ]'
          ]),
        ]),
      ]),
      m("div#output", { onupdate: scrollOutput }, m("pre", output))
    ]);
  };
  
  loadStockfish();

  return { oninit, view };
};

if (!isSupported()) {
  window.alert("Your browser is not supported. For more information, please take a look at https://github.com/hi-ogawa/Stockfish/wiki.");
} else {
  m.mount($("#root"), App);
}

</script>
</body>
